#include "p2p/basicpacketsocketfactory.h"

#include "base/asyncudpsocket.h"
#include "base/asynctcpsocket.h"
#include "base/socketadapters.h"
#include "p2p/asyncstuntcpsocket.h"
#include "base/thread.h"
#include "base/logging.h"

namespace base
{
	BasicPacketSocketFactory::BasicPacketSocketFactory()
		: thread_(Thread::Current()), socket_factory_(NULL)
	{
	}

	BasicPacketSocketFactory::BasicPacketSocketFactory(Thread* thread)
		: thread_(thread), socket_factory_(NULL)
	{
	}

	BasicPacketSocketFactory::BasicPacketSocketFactory(SocketFactory* socket_factory)
		: thread_(NULL), socket_factory_(socket_factory)
	{
	}

	BasicPacketSocketFactory::~BasicPacketSocketFactory()
	{
	}
	
	AsyncPacketSocket* BasicPacketSocketFactory::CreateUdpSocket(
		const SocketAddress& address, int min_port, int max_port)
	{
		// UDP sockets are simple
		AsyncSocket* socket = socket_factory()->CreateAsyncSocket(
			address.family(), SOCK_DGRAM);
		if (!socket)
			return NULL;

		if (BindSocket(socket, address, min_port, max_port) < 0)
		{
			LOG(LS_ERROR) << "UDP bind failed with error " << socket->GetError();
			delete socket;
			return NULL;
		}

		return new AsyncUDPSocket(socket);
	}

	AsyncPacketSocket* BasicPacketSocketFactory::CreateServerTcpSocket(
		const SocketAddress& local_address, int min_port, int max_port, int opts)
	{
		AsyncSocket* socket = socket_factory()->CreateAsyncSocket(
			local_address.family(), SOCK_STREAM);
		if (!socket)
			return NULL;

		if (BindSocket(socket, local_address, min_port, max_port) < 0)
		{
			LOG(LS_ERROR) << "TCP bind failed with error "
				<< socket->GetError();
			delete socket;
			return NULL;
		}

		// If using SSLTCP, wrap the TCP socket in a pseudo-SSL socket.
		if (opts & PacketSocketFactory::OPT_SSLTCP)
			socket = new AsyncSSLSocket(socket);

		// Set TCP_NODELAY (via OPT_NODELAY) for improved performance.
		socket->SetOption(Socket::OPT_NODELAY, 1);

		if (opts & PacketSocketFactory::OPT_STUN)
			return new cricket::AsyncStunTCPSocket(socket, true);

		return new AsyncTCPSocket(socket, true);
	}

	AsyncPacketSocket* BasicPacketSocketFactory::CreateClientTcpSocket(
		const SocketAddress& local_address, const SocketAddress& remote_address, 
		const ProxyInfo& proxy_info, const std::string& user_agent, int opts)
	{
		AsyncSocket* socket = socket_factory()->CreateAsyncSocket(
			local_address.family(), SOCK_STREAM);
		if (!socket)
			return NULL;

		if (BindSocket(socket, local_address, 0, 0) < 0)
		{
			LOG(LS_ERROR) << "TCP bind failed with error "
				<< socket->GetError();
			delete socket;
			return NULL;
		}

		// If using a proxy, wrap the socket in a proxy socket.
		switch (proxy_info.type)
		{
		case PROXY_SOCKS5:
			socket = new AsyncSocksProxySocket(socket,
				proxy_info.address, proxy_info.username, proxy_info.password);
			break;

		case PROXY_HTTPS:
			socket = new AsyncHttpsProxySocket(socket, user_agent,
				proxy_info.address, proxy_info.username, proxy_info.password);
			break;
		}

		// If using SSLTCP, wrap the TCP socket in a pseudo-SSL socket.
		if (opts & PacketSocketFactory::OPT_SSLTCP)
			socket = new AsyncSSLSocket(socket);

		if (socket->Connect(remote_address) < 0)
		{
			LOG(LS_ERROR) << "TCP connect failed with error "
				<< socket->GetError();
			delete socket;
			return NULL;
		}

		// Finally, wrap that socket in a TCP or STUN TCP packet socket.
		AsyncPacketSocket* tcp_socket;
		if (opts & PacketSocketFactory::OPT_STUN)
			tcp_socket = new cricket::AsyncStunTCPSocket(socket, false);
		else
			tcp_socket = new AsyncTCPSocket(socket, false);

		// Set TCP_NODELAY (via OPT_NODELAY) for improved performance.
		tcp_socket->SetOption(Socket::OPT_NODELAY, 1);

		return tcp_socket;
	}

	int BasicPacketSocketFactory::BindSocket(
		AsyncSocket* socket, const SocketAddress& local_address, 
		int min_port, int max_port)
	{
		int ret = -1;
		if (min_port == 0 && max_port == 0)
		{
			// If there's no port range, let the OS pick a port for us.
			ret = socket->Bind(local_address);
		}
		else
		{
			// Otherwise, try to find a port in the provided range.
			for (int port = min_port; ret < 0 && port <= max_port; ++port)
				ret = socket->Bind(SocketAddress(local_address.ipaddr(), port));
		}
		return ret;
	}

	SocketFactory* BasicPacketSocketFactory::socket_factory()
	{
		if (thread_)
		{
			ASSERT(thread_ == Thread::Current());
			return thread_->socketserver();
		}
		else
			return socket_factory_;
	}
}